#include "luaglfw.h"
#include "GLFW/glfw3.h"
#include "log.h"

extern "C"
{
#include "lauxlib.h"
}

static bool luaL_checkboolean(lua_State *L, int arg)
{
    if (lua_isboolean(L, arg))
        luaL_argerror(L, arg, "boolean expected");
    else
        return lua_toboolean(L, arg);
    return false;
}

static GLFWmonitor *luaL_checkmonitor(lua_State *L, int arg)
{
    auto monitor = static_cast<GLFWmonitor *>(lua_touserdata(L, arg));
    if (!monitor)
        luaL_typeerror(L, arg, "GLFWmonitor");
    return monitor;
}

GLFWmonitor *luaL_optmonitor(lua_State *L, int args, GLFWmonitor *def = NULL)
{
    return luaL_opt(L, luaL_checkmonitor, args, def);
}

static GLFWwindow *luaL_checkwindow(lua_State *L, int arg)
{
    auto monitor = static_cast<GLFWwindow *>(lua_touserdata(L, arg));
    if (!monitor)
        luaL_typeerror(L, arg, "GLFWmonitor");
    return monitor;
}

GLFWwindow *luaL_optwindow(lua_State *L, int args, GLFWwindow *def = NULL)
{
    return luaL_opt(L, luaL_checkwindow, args, def);
}

#define lua_pushmonitor(L, v) lua_pushlightuserdata(L, v)
#define lua_pushwindow(L, v) lua_pushlightuserdata(L, v)

static int CreateWindow(lua_State *L)
{
    int width = (int)luaL_checkinteger(L, 1);
    int height = (int)luaL_checkinteger(L, 2);
    const char *title = luaL_optstring(L, 3, "");
    GLFWmonitor *monitor = luaL_optmonitor(L, 4);
    GLFWwindow *share = luaL_optwindow(L, 5);
    GLFWwindow *win = glfwCreateWindow(width, height, title, monitor, share);
    if (!win)
        return luaL_error(L, "cannot create window");
    lua_pushlightuserdata(L, win);
    return 1;
}

static int DestroyWindow(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwDestroyWindow(win);
    return 0;
}

static int FocusWindow(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwFocusWindow(win);
    return 0;
}

static int SetWindowAspectRatio(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    int numer = (int)luaL_optinteger(L, 2, GLFW_DONT_CARE);
    int denom = (int)luaL_optinteger(L, 3, GLFW_DONT_CARE);
    glfwSetWindowAspectRatio(win, numer, denom);
    return 0;
}

static int SetWindowSizeLimits(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    int minwidth = (int)luaL_optinteger(L, 2, GLFW_DONT_CARE);
    int minheight = (int)luaL_optinteger(L, 3, GLFW_DONT_CARE);
    int maxwidth = (int)luaL_optinteger(L, 4, GLFW_DONT_CARE);
    int maxheight = (int)luaL_optinteger(L, 5, GLFW_DONT_CARE);
    glfwSetWindowSizeLimits(win, minwidth, minheight, maxwidth, maxheight);
    return 0;
}

static int SetWindowIcon(lua_State *L)
{
    size_t len;
    int count, arg;
    GLFWimage images[32];
    auto win = luaL_checkwindow(L, 1);
    count = 0;
    arg = 2;
    while (!lua_isnoneornil(L, arg))
    {
        images[count].width = (int)luaL_checkinteger(L, arg++);
        images[count].height = (int)luaL_checkinteger(L, arg++);
        images[count].pixels = (unsigned char *)luaL_checklstring(L, arg++, &len);
        count++;
    }
    if (count == 0)
        luaL_checkinteger(L, arg); /* raises an error */
    glfwSetWindowIcon(win, count, images);
    return 0;
}

static int SetWindowMonitor(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    GLFWmonitor *monitor = luaL_optmonitor(L, 2);
    int xpos = (int)luaL_checkinteger(L, 3);
    int ypos = (int)luaL_checkinteger(L, 4);
    int width = (int)luaL_checkinteger(L, 5);
    int height = (int)luaL_checkinteger(L, 6);
    int refreshRate = (int)luaL_optinteger(L, 7, GLFW_DONT_CARE);
    glfwSetWindowMonitor(win, monitor, xpos, ypos, width, height, refreshRate);
    return 0;
}

static int PollEvents(lua_State *L)
{
    (void)L;
    glfwPollEvents();
    return 0;
}

static int WaitEvents(lua_State *L)
{
    (void)L;
    glfwWaitEvents();
    return 0;
}

static int WaitEventsTimeout(lua_State *L)
{
    double timeout = luaL_checknumber(L, 1);
    glfwWaitEventsTimeout(timeout);
    return 0;
}

static int PostEmptyEvent(lua_State *L)
{
    (void)L;
    glfwPostEmptyEvent();
    return 0;
}

static int SwapBuffers(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwSwapBuffers(win);
    return 0;
}

static int WindowShouldClose(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    lua_pushboolean(L, glfwWindowShouldClose(win));
    return 1;
}

static int SetWindowShouldClose(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    int value = luaL_checkboolean(L, 2);
    glfwSetWindowShouldClose(win, value);
    return 0;
}

static int SetWindowTitle(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    const char *title = luaL_optstring(L, 2, "");
    glfwSetWindowTitle(win, title);
    return 0;
}

static int GetWindowTitle(lua_State *L)
{
    const char *title;
    auto win = luaL_checkwindow(L, 1);
    title = glfwGetWindowTitle(win);
    lua_pushstring(L, title);
    return 1;
}

static int GetWindowPos(lua_State *L)
{
    int xpos, ypos;
    auto win = luaL_checkwindow(L, 1);
    glfwGetWindowPos(win, &xpos, &ypos);
    lua_pushinteger(L, xpos);
    lua_pushinteger(L, ypos);
    return 2;
}

static int SetWindowPos(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    int xpos = (int)luaL_checkinteger(L, 2);
    int ypos = (int)luaL_checkinteger(L, 3);
    glfwSetWindowPos(win, xpos, ypos);
    return 0;
}

static int GetWindowSize(lua_State *L)
{
    int width, height;
    auto win = luaL_checkwindow(L, 1);
    glfwGetWindowSize(win, &width, &height);
    lua_pushinteger(L, width);
    lua_pushinteger(L, height);
    return 2;
}

static int SetWindowSize(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    int width = (int)luaL_checkinteger(L, 2);
    int height = (int)luaL_checkinteger(L, 3);
    glfwSetWindowSize(win, width, height);
    return 0;
}

static int GetFramebufferSize(lua_State *L)
{
    int width, height;
    auto win = luaL_checkwindow(L, 1);
    glfwGetFramebufferSize(win, &width, &height);
    lua_pushinteger(L, width);
    lua_pushinteger(L, height);
    return 2;
}

static int GetWindowFrameSize(lua_State *L)
{
    int left, top, right, bottom;
    auto win = luaL_checkwindow(L, 1);
    (void)L;
    glfwGetWindowFrameSize(win, &left, &top, &right, &bottom);
    lua_pushinteger(L, left);
    lua_pushinteger(L, top);
    lua_pushinteger(L, right);
    lua_pushinteger(L, bottom);
    return 4;
}

static int IconifyWindow(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwIconifyWindow(win);
    return 0;
}

static int RestoreWindow(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwRestoreWindow(win);
    return 0;
}

static int MaximizeWindow(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwMaximizeWindow(win);
    return 0;
}

static int ShowWindow(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwShowWindow(win);
    return 0;
}

static int HideWindow(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwHideWindow(win);
    return 0;
}

static int GetWindowMonitor(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    GLFWmonitor *monitor = glfwGetWindowMonitor(win);
    if (!monitor)
        return 0;
    lua_pushmonitor(L, monitor);
    return 1;
}

static int GetWindowContentScale(lua_State *L)
{
    float xscale, yscale;
    auto win = luaL_checkwindow(L, 1);
    glfwGetWindowContentScale(win, &xscale, &yscale);
    lua_pushnumber(L, xscale);
    lua_pushnumber(L, yscale);
    return 2;
}

static int GetWindowOpacity(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    lua_pushnumber(L, glfwGetWindowOpacity(win));
    return 1;
}

static int SetWindowOpacity(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    auto opacity = (float)luaL_checknumber(L, 2);
    glfwSetWindowOpacity(win, opacity);
    return 0;
}

static int RequestWindowAttention(lua_State *L)
{
    auto win = luaL_checkwindow(L, 1);
    glfwRequestWindowAttention(win);
    return 0;
}

static int Init(lua_State *L)
{
    glfwInit();
    return 0;
}

static int WindowHint(lua_State *L)
{
    auto hint = (int)luaL_checkinteger(L, 1);
    auto value = (int)luaL_checkinteger(L, 2);
    glfwWindowHint(hint, value);
    return 0;
}

static const struct luaL_Reg functions[] = {
    {"Init", Init},
    {"WindowHint", WindowHint},
    {"CreateWindow", CreateWindow},
    {"ShowWindow", ShowWindow},
    {"DestroyWindow", DestroyWindow},
    {"FocusWindow", FocusWindow},
    {"SetWindowAspectRatio", SetWindowAspectRatio},
    {"SetWindowSizeLimits", SetWindowSizeLimits},
    {"SetWindowIcon", SetWindowIcon},
    {"SetWindowMonitor", SetWindowMonitor},
    {"PollEvents", PollEvents},
    {"WaitEvents", WaitEvents},
    {"WaitEventsTimeout", WaitEventsTimeout},
    {"PostEmptyEvent", PostEmptyEvent},
    {"SwapBuffers", SwapBuffers},
    {"WindowShouldClose", WindowShouldClose},
    {"SetWindowShouldClose", SetWindowShouldClose},
    {"SetWindowTitle", SetWindowTitle},
    {"GetWindowTitle", GetWindowTitle},
    {"GetWindowPos", GetWindowPos},
    {"SetWindowPos", SetWindowPos},
    {"GetWindowSize", GetWindowSize},
    {"SetWindowSize", SetWindowSize},
    {"GetFramebufferSize", GetFramebufferSize},
    {"GetWindowFrameSize", GetWindowFrameSize},
    {"IconifyWindow", IconifyWindow},
    {"RestoreWindow", RestoreWindow},
    {"MaximizeWindow", MaximizeWindow},
    {"ShowWindow", ShowWindow},
    {"HideWindow", HideWindow},
    {"GetWindowMonitor", GetWindowMonitor},
    {"GetWindowContentScale", GetWindowContentScale},
    {"GetWindowOpacity", GetWindowOpacity},
    {"SetWindowOpacity", SetWindowOpacity},
    {"RequestWindowAttention", RequestWindowAttention},
    {nullptr, nullptr} /* sentinel */
};

static void glfw_error_callback(int error, const char *description)
{
    LogE("[GLFW] Error {}: {}\n", (int)error, description);
}

int luaopen_glfw(lua_State *L)
{
    glfwSetErrorCallback(glfw_error_callback);
    luaL_newlib(L, functions);
    return 1;
}